/* CS32 FINAL PROJECT */

package GameEngine;

import GameEngine.GameConstants.Action;
import Graphics.ServerGUI;
import Objects.BasicObject;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.swing.DefaultListModel;
import javax.swing.Timer;

/**
 * SERVER/CLIENT PROTOCOL:
 * 
 * Upon Connection, Server waits for Client to verify they are ready to play by listening for a byte.
 *      Client: sends special (byte)
 *      Server: sends ID (short)
 *      Server: sends level background reference (byte)
 *      Server: sends level width (int)
 *      Server: sends level height (int)
 * When all Clients are ready to play the server sends a ready indication to all connected clients
 *      Server: sends game beginning (boolean = true)
 * Once the game begins, the client continuously reads for the state of the game from the server.
 *      Server: sends the initial state of all objects in level to all connected clients
 *          ****** LOOP *******
 *          idnum       : short
 *          graphic     : byte
 *          X direction : float
 *          Y direction : float
 *          X center    : short
 *          Y center    : short
 *          X size      : short
 *          Y Size      : short
 *          exists?     : boolean
 *      Server: sends end of objects (boolean = true)
 *      Client: continuously reads this data above
 * At this point, the server sends at timed intervals the state of the game with the protocol above.
 * Meanwhile, the IncomingClientHandler is continuously reading for actions from the associated client.
 * When an action is received, the information is passed into the calculation for the new state of the level
 *      Client: able to send actions (byte)
 *      Server: continuously reads for actions (bytes)
 * When level calculates a victory condition, the server sends a dummy object with ID = -7. 
 * When client reads this, they set variable that prevents them from sending anymore actions to the server
 * Client then responds acknowledging they know level is over.
 *      Server: sends dummy object (short = -7)
 *      Client: sends acknowledgment (byte = -7)
 * Currently when this happens the server closes and all users are sent back to the main menu.  If work continues on this project
 * we will alter the events following victory condition.
 */

/**
 * GameServer listens for clients and accepts connections.  Once connected, an IncomingClientHandler (ICH) is associated with the
 * client socket.  The ICH handles IO communication between the server and the client.  Once all clients are connected and ready
 * to play, the server initiates begins updating the level and sending the updated state to each connected client.  The game
 * continues until the server stops or a victory condition is met.  Victory occurs when the level reports an object with ID = -7.
 * The server manages communication between players and the game world.
 *
 * @author crdunwel
 */
public class GameServer extends Thread
{
    // STATIC VARIABLES
   public static final int TIME_INTERVAL = 40;              // Update the client graphics every 50 milliseconds

    // INSTANCE VARIABLES
    private int localport;                                  // The port for the server to listen on.
    public ServerSocket serverSocket;                       // The socket the server will accept connections on.
    public List<UserAction> parsedActions;                  // The list of Actions from the clients.
    private List<IncomingClientHandler> clients;            // The list of currently connected clients.
    private List<IncomingClientHandler> clientsToRemove;    // The list of clients to remove.
    public Level level;                                     // The server's reference to the level object.
    public byte levelbg;                                    // The byte representing the corresponding level in GameConstants ENUM
    private Timer updateClientTimer;                        // The timer that updates the clients with the state of the level
    private DefaultListModel log;                           // The log to write to document ocurrences on the server
    private ServerGUI guireference;                         // The reference to the GUI which graphically display what happens in server

    /**
     * CONSTRUCTOR
     * 
     * The GameServer is the communication middle-man for communicating level
     * information with connected clients
     *
     * @param level                     the level the server is running
     * @param portnum                   port number for ServerSocket
     */
    public GameServer(Level lvl, int portnum, byte lname, ServerGUI gui)
    {
            guireference = gui;                                         // reference to the GUI that displays server information
            level = lvl;                                                // reference to level that server is hosting
            levelbg = lname;                                            // byte representation for level background
            localport = portnum;                                        // port number for server
            clients = new ArrayList<IncomingClientHandler>();           // handles IO with Clients
            parsedActions = new ArrayList<UserAction>();                // actions parsed from users
            clientsToRemove = new ArrayList<IncomingClientHandler>();   // clients to remove from server
            try
            {
                serverSocket = new ServerSocket(localport);         // server socket for IO with Clients
            }
            catch (IOException e1)
            {
                System.out.println("GameServer Error: Failed to create ServerSocket");
            }
    }
    
    /**
     * RUN METHOD
     * 
     * This is the run method for the GameServer. 
     */
    @Override
    public void run()
    {
        try
        {
            // log server starting
            writeLog("Server started at " + InetAddress.getLocalHost().getHostAddress()+":"+localport+".");
            // loop to connect all clients to server
            connectClients();
            // loop to check if clients are ready to play
            checkClientsReady();
            // send initial level to all clients
            sendInitialLevel();
            // loop to update game state
            updateClientGame();
        }    
        catch (IOException e3)
        {
            System.out.println("GameServer Error: server run thread error");
        }
    }
    
    /**
     * This method writes a message to the server log to be displayed graphically in ServerGUI
     */
    public void writeLog(String message)
    {
        if (log != null)
        {
            DateFormat df = new SimpleDateFormat("HH:mm:ss");
            String timeStamp = df.format(new Date());
	    log.addElement(timeStamp + ": " + message);
        }
    }
    
    /**
     * This method connects users to the server and adding them to the level
     */
    public void connectClients()
    {
        Socket clientSocket;
        IncomingClientHandler handler;
        byte currNumPlayers = 0;
        int maxPlayers = level.getNumPlayers();
        while (currNumPlayers < maxPlayers)
        {
            try
            {
                // accept client to server
                clientSocket = serverSocket.accept();
                // write to log file that user connected
                writeLog("User connected.");
                // create handler for client
                handler = new IncomingClientHandler(this,clientSocket);
                // adds client to handler list
                clients.add(handler);
                // starts handler
                handler.start();
                // increments number of players currently on server
                currNumPlayers++;
            }
            catch (IOException e1)
            {
                writeLog("GameServer Error: Problm connecting user to server.");
            }
        }
    }
    
    /**
     * This method checks to see if all the clients are ready to play by looping through their respective
     * IncomingClientHandler and checking if they're ready to play.  If at least one client is not ready, the
     * loop continues.  The loop only breaks if all clients are ready.
     */
    public void checkClientsReady()
    {
        boolean allReady = false;
        while (!allReady)
        {
            // assume all clients are ready
            allReady = true;
            // check if all clients are actually ready
            for (IncomingClientHandler ICH : clients)
            {
                // true if client is not ready to play
                if (!ICH.isReadyToPlay())
                {
                    // not ready to start game
                    allReady = false;
                }
            }
        }
        for (IncomingClientHandler ICH : clients)
        {
            // inform all clients that server is ready to start
            ICH.sendUserServerReady();
        }
        // write log message
        writeLog("All users ready, starting game.");
    }
    
    /**
     * This method sends the initial level to all the connected and ready clients
     */
    public void sendInitialLevel()
    {
        List<BasicObject> toSend = level.getBasics();
        for (IncomingClientHandler ICH : clients)
        {
            ICH.updateLevel(toSend);
        }
    }
    
    /**
     * This method initialize a timer which executes 
     * enters an infinite loop, updating the state of the level from the parsedActions
     * from users and sends the updated level to each user via IO streams.
     */
    public void updateClientGame()
    {
        updateClientTimer = new Timer(TIME_INTERVAL, null);
        updateClientTimer.addActionListener(new ServerListener());
        updateClientTimer.start();
    }
    
    /**
     * This private class handles the action for the timer initiated in updateClientGame()
     */
    private class ServerListener implements ActionListener
    {
        /**
         * This method updates the state of the level and sends it to connected clients
         * @param e 
         */
        public void actionPerformed(ActionEvent e)
        {
            // updates state of level and stores it in a list to send to connected clients
            List<BasicObject> toSend = updateLevelState();
            for (IncomingClientHandler ICH : clients)
            {
                // sends level update to each client
                ICH.updateLevel(toSend);
            }
            // remove clients that disconnected from the server
            removeClientsFromServer();
            // handles victory condition
            if (toSend.size() > 0 && toSend.get(0).getId() == -7)
            {
                // flush data
                parsedActions.clear();
                clients.clear();
                try
                {
                    serverSocket.close();
                }
                catch (IOException e1)
                {
                    System.out.println("GameServer Error: unable to close serverSocket");
                }
                writeLog("Game Finished");
                updateClientTimer.stop();
                guireference.dispose();
            }
        }
    }
       
    /**
     * This method generates the new state of the level with the input of the clients.
     * @return  list of BasicObject to send to clients
     */
    public synchronized List<BasicObject> updateLevelState()
    {
        List<BasicObject> toReturn = level.updateState(parsedActions);
        parsedActions.clear();
        return toReturn;
    }
    
    /**
     * This method stores actions from users in a list.
     * The actions are sent to the level to be executed.
     *
     * @param act   The action from the client
     * @param id    The ID for the client sending the action
     */
    public synchronized void userAction(Action act, short id)
    {
        parsedActions.add (new UserAction(act, id));
    }
    
    /**
     * This method removes all the clients that disconnected from the server.
     */
    public void removeClientsFromServer()
    {
        for (IncomingClientHandler ICH: clientsToRemove)
        {
            if (clients.contains(ICH))
            {
                clients.remove(ICH);
            }
        }
    }
   
    /**
     * This method adds a client that disconnected from the server to a list in order to be removed later.
     *
     * @param ich   IncomingClientHandler to remove from server
     */
    public synchronized void removeClient(IncomingClientHandler ich)
    {
        writeLog("Client disconnected");
        clientsToRemove.add(ich);
    }

    /**
     * This method re-starts the thread for each ICH.  It is called to re-start the level after victory occurs.
     */
    public void restartClientHandlers()
    {
        for (IncomingClientHandler ICH : clients)
        {
            ICH.start();
        }
    }
    
    // GETTERS AND SETTERS
    /******************************************************************/
    public Level getLevel()
    {
        return level;
    }

    public List<IncomingClientHandler> getClients()
    {
        return clients;
    }

    public byte getLevelBG()
    {
        return levelbg;
    }
    
    public void setLog(DefaultListModel logModel)
    {
        log = logModel;
    }

    public ServerSocket getServerSocket()
    {
        return serverSocket;
    }
    /******************************************************************/
}
